package core;

import static view.Constants.BUFFER_SIZE;
import static view.Constants.DEFAULT_TABLE;
import static view.Constants.MAPPING;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import controller.Controller;

import testing.FolderCompresser;

/**
 * @author Mostafa Mahmod Mahmod Eweda
 * <pre>mostafa_eweda17@yahoo.com</pre>
 * @since JDK 1.6
 * @version 1.0
 * 
 * The parent of the compressors that has the common fields and functions provided
 * by the AbstractCompresser
 * 
 * @see FileCompresser
 * @see FolderCompresser
 */
public class FileDecompresser
{
	/**
	 * array of masks to be used in getting the data from bytes
	 */
	private int[] oneMasks;

	/**
	 * the index of the out buffer
	 */
	private int iOut;

	/**
	 * The out buffer used in writing the output file
	 */
	private byte[] outBuffer;

	/**
	 * The padding in the last byte
	 */
	private byte padding;

	/**
	 * The root of the huffman tree got from the canonical code
	 */
	private Node root;

	/**
	 * The input stream on the compressed file
	 */
	private BufferedInputStream in;

	/**
	 * The output stream to the de-compressed file
	 */
	private FileChannel writer;

	/**
	 * The current node traversing the tree
	 */
	private Node temp;

	private File compressedFile;

	private String decompressPath;

	/**
	 * Creates A FileDecompresser with the given compressed and de-compressed file paths
	 * @param compressed the path of the compressed file
	 * @param decompressPath the trimmed path of the compressed file wanted to be created
	 */
	public FileDecompresser(String compressed, String decompressPath)
	{
		this.decompressPath = decompressPath;
		compressedFile = new File(compressed);
		outBuffer = new byte[BUFFER_SIZE];
		try {
			in = new BufferedInputStream(new FileInputStream(compressedFile));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		oneMasks = new int[32];
		for (int i = 0; i < oneMasks.length; i++) {
			oneMasks[i] = 1 << i;
		}
	}

	/**
	 * creates the tree with the given bytes as a data source of the table
	 * @param tableDat
	 */
	public void constructTree(byte[] tableDat)
	{
		root = new Node();
		CompressedChar[] compChars = new CompressedChar[tableDat.length];
		Node temp = null;
		long res = -1;
		byte ch;
		long prevLen = tableDat[1];
		long andRes;
		int len;
		for (int i = 0, n = tableDat.length / 2; i < n; i++)
		{
			res += 1;
			ch = tableDat[2 * i];
			len = tableDat[2 * i + 1];
			if (len < prevLen)
			{
				// NOTE the usage of the tripple shifting operator
				res = res >>> (prevLen - len);
			}
			prevLen = len;
			temp = root;
			for (int j = len - 1; j >= 1; j--)
			{
				andRes = res & oneMasks[j];
				if (andRes == 0) {
					if (!temp.hasLeft())
						temp.setLeft(new Node());
					temp = temp.left();
				} else {
					if (!temp.hasRight())
						temp.setRight(new Node());
					temp = temp.right();
				}
			}
			// repeated but efficient code
			andRes = res & oneMasks[0];
			if (andRes == 0) {
				temp.setLeft(new Node(ch));
			} else {
				temp.setRight(new Node(ch));
			}
			compChars[i] = new CompressedChar(res, (byte) len, ch);
		}
		// System.out.println();
		this.temp = root;
	}

	public void decompress() {
		try {
			long reached = 0;
			byte[] buffer = new byte[BUFFER_SIZE];
			int numRead = 0;
			while ((numRead = in.read(buffer)) != -1) {
				if (numRead < BUFFER_SIZE)
					numRead--;
				convertChars(buffer, numRead, 0);
				reached += BUFFER_SIZE;
				Controller.getInstance().updateReached(reached, compressedFile.length());
				if (numRead < BUFFER_SIZE)
					convertChars(new byte[] { buffer[numRead] }, 1,
							padding);
			}
			writer.write(ByteBuffer.wrap(outBuffer, 0, iOut));
			in.close();
			writer.close();
			reached = compressedFile.length();
			Controller.getInstance().updateReached(reached, compressedFile.length());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void convertChars(byte[] buffer, int numRead, int columns)
	{
		for (int i = 0; i < numRead; i++)
		{
			byte b = buffer[i];
			for (int j = 7; j >= columns; j--)
			{
				int mask = oneMasks[j];
				int result = b & mask;
				if (result != 0) {
					temp = temp.right();
				} else // zero
				{
					temp = temp.left();
				}
				if (temp.isExternal())
				{
					if (iOut == BUFFER_SIZE) {
						iOut = 0;
						try {
							writer.write(ByteBuffer.wrap(outBuffer));
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
					outBuffer[iOut++] = (byte) (temp.character() + MAPPING);
					// System.out.print((char)
					// (temp.character()+FileAnalysis.MAPPING));
					temp = root;
				}
			}
		}
	}

	/**
	 * Reads the table from the file and configures it if it is written
	 * or regenerates it from the default table it wasn't saved
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public void recognizeTable() throws IOException, ClassNotFoundException
	{
		byte[] initialData = new byte[2];
		byte[] tableDat = null;
		in.read(initialData);
		int tableLength = 0;
		padding = initialData[0];
		if (initialData[1] == 1) // the default table is defined
		{
			tableLength = in.read();
			if (tableLength <= 0)
				tableLength += 256;
			tableDat = new byte[2 * tableLength];
			in.read(tableDat);
		}
		else // the table is save with the file
		{
			ObjectInputStream inTable;
			inTable = new ObjectInputStream(new FileInputStream(DEFAULT_TABLE));
			CompressedChar[] defaultTable = (CompressedChar[]) inTable.readObject();

			tableDat = new byte[defaultTable.length * 2];

			for (int i = 0; i < defaultTable.length; i++)
			{
				tableDat[2 * i] = defaultTable[i].ch;
				tableDat[2 * i + 1] = defaultTable[i].length;
			}
			inTable.close();
		}
		constructTree(tableDat);
	}

	/**
	 * starts the configuration of the table
	 */
	public void registerTable()
	{
		try {
			recognizeTable();
			readName();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}

	/**
	 * reads the name of the de-compressed file to extract the file to its old extension
	 * @throws IOException
	 */
	private void readName() throws IOException
	{
		String decompressed = "";
		byte[] buff = new byte[1];
		while (buff[0] != 10) // the new line feed {
		{
			in.read(buff);
			char temp = (char) buff[0];
			if (buff[0] < 0)
				temp += 256;
			decompressed += temp;
		}
		decompressed = decompressed.substring(0, decompressed.length() - 1);
		FileOutputStream opStream;
		opStream = new FileOutputStream(decompressPath + File.separatorChar + decompressed);
		writer = opStream.getChannel();
	}
}
